#!/usr/bin/env python
#
# host_group_tab.py
#
# Version : 0.7
# Date : 20080529
#
# Author : Bernard Czenkusz
# Email  : bernie@skipole.co.uk

# host_group_tab.py - Displays the hosts and groups and is a module in
# the sm_gui package, run as part of the skipolemonitor gui interface
#
# Copyright (c) 2008 Bernard Czenkusz
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#

""" This is a module in the sm_gui package, and defines the Host and Group tab

It is run as part of the skipolemonitor gui interface. It includes buttons to
load and save the network, a split window with a tree of items on the left and
the host and group menus on the right.
"""


import wx
import os
import cPickle
import makehtml
from net import *
from sm_gui import gui_globals
from sm_gui import group_menu
from sm_gui import host_menu


class HostGroupSetup(wx.Panel):
    """ This panel holds New, Save .. etc buttons, and a split panel beneath them
        in its turn the split panel holds the item tree on the left, and the
        group or host setup menu on the right depending on which item is selected"""

    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        """Create the panel, with a list of buttons at the top, and a split panel beneath"""
        # Create the main sizer, splitting the Panel vertically
        self.main_sizer=wx.BoxSizer(wx.VERTICAL)
        # Create the list of top buttons, inside button_box_sizer
        self.setup_top_buttons()
        # Add button_box_sizer to the main vertical sizer
        self.main_sizer.Add(self.button_box_sizer, 0,  wx.BOTTOM | wx.TOP | wx.EXPAND, 10)
        #Create a split window
        self.sp=SMSplitterWindow(self)        
        # Add the Split panel to the sizer
        self.main_sizer.Add(self.sp, 1,  wx.EXPAND)
        # Set the size to the panel, and frame
        self.SetSizer(self.main_sizer)

    def setup_top_buttons(self):
        """ Define the top set of buttons - New Save Load Import Help"""
        # Set the button size
        button_size = (80,40)
        # Create a horizontal button_box_sizer to hold the buttons
        self.button_box_sizer=wx.BoxSizer(wx.HORIZONTAL)
        # Define buttons in a list of lists, each element being the button label
        # and the method the button calls when pressed
        self.top_buttons = [["New", self.new_button_pressed],
                            ["Save", self.save_button_pressed],
                            ["Load", self.load_button_pressed],
                            ["Import", self.import_button_pressed],
                            ["Help", self.help_button_pressed]]
        # Create the buttons, by appending a wxButton object to each list item
        for button in self.top_buttons:
            button.append(wx.Button(self, label=button[0], size=button_size))
            # Add space and buttons to sizer
            self.button_box_sizer.Add((20,40), 1)
            self.button_box_sizer.Add(button[2], 0)
            # Create Button Bindings
            self.Bind(wx.EVT_BUTTON, button[1], button[2])
        # Add a final right-hand most spacer to the row of buttons
        self.button_box_sizer.Add((20,40), 1)
        return

    # ------------- Top button methods

    def new_button_pressed(self, event):
        """New button - deletes current network, refreshes tree"""
        if net_reload(): return
        retCode=wx.MessageBox(parent=self,
                              message="This deletes the current network.\nAre you sure!",
                              caption="Delete Network",
                              style=wx.OK | wx.CANCEL | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP)
        if retCode != wx.OK : return
        # Flag to other threads, that the network is being reloaded
        set_net_reload()
        # Display a please wait message in the right menu area
        self.sp.display_please_wait()
        # clear out the html folder
        makehtml.new_httproot()
        # Creates a network loading html page
        makehtml.network_loading()
        # self.delete_net_renew_tree is called 5 seconds from now,
        # since if we called delete_network() from here, this function would not return
        # until the job was done, and the app loop would not display the required message
        # The time delay also allows other threads to note that the network is reloading
        wx.FutureCall(5000, self.sp.delete_net_renew_tree)

    def save_button_pressed(self, event):
        """Save button pressed - user can take a copy of the network file"""
        if net_reload(): return
        # Create the save file dialog.
        # Pre-establish a file filter to show the .dat extension
        wildcard = "data file (*.dat)|*.dat|All files (*.*)|*.*"
        dlg = wx.FileDialog(self, message="Save file as ...",
                                  defaultDir=net_cfg["EXTERNAL_NETWORKS"],
                                  defaultFile="",
                                  wildcard=wildcard,
                                  style=wx.SAVE|wx.OVERWRITE_PROMPT)
        # Show the dialog and retrieve the user response.
        # If it is the OK response, save the network data.
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            fp=None
            try:
                fp=open(path,"w")
                p=cPickle.Pickler(fp)
                p.dump(whole_network())
                fp.close()
                wx.MessageBox(parent=self,
                              message="Your file has been saved",
                              caption="Saved",
                              style=wx.OK|wx.ICON_INFORMATION | wx.STAY_ON_TOP)
            except:
                if fp: fp.close()
                wx.MessageBox(parent=self,
                              message="ERROR: Unable to save file",
                              caption="ERROR",
                              style=wx.OK|wx.ICON_ERROR | wx.STAY_ON_TOP)
        dlg.Destroy()

    def load_button_pressed(self, event):
        """Load button pressed, user loads in a previously saved network"""
        if net_reload(): return
        retCode=wx.MessageBox(parent=self,
                              message="This overwrites the current network.\nAre you sure!",
                              caption="Overwrite Network",
                              style=wx.OK | wx.CANCEL | wx.ICON_EXCLAMATION | wx.STAY_ON_TOP)
        if retCode != wx.OK : return
        # Flag to other threads, that the network is being reloaded
        set_net_reload()
        # Create the load file dialog.
        # Pre-establish a file filter to show the .dat extension
        wildcard = "data file (*.dat)|*.dat|All files (*.*)|*.*"
        dlg = wx.FileDialog(self, message="Load file...", defaultDir=net_cfg["EXTERNAL_NETWORKS"],
                            defaultFile="", wildcard=wildcard, style=wx.OPEN)
        # Show the dialog and retrieve the user response.
        # If it is the OK response, save the network data.
        if dlg.ShowModal() == wx.ID_OK:
            filepath = dlg.GetPath()
            dlg.Destroy()
        else:
            unset_net_reload()
            return
        # Display a please wait message in the right menu area
        self.sp.display_please_wait()
        # clear out the html folder
        makehtml.new_httproot()
        # Creates a network loading html page
        makehtml.network_loading()
        # self.load_net_renew_tree is called 5 seconds from now,
        # since if we called load_saved_network() from here, this function would not return
        # until the job was done, and the app loop would not display the required message
        # The time delay also allows other threads to note that the network is reloading
        wx.FutureCall(5000, self.sp.load_net_renew_tree, filepath)

    def import_button_pressed(self, event):
        """Import button pressed - display the csv import window"""
        import_box = ImportCSV(self)
        retCode = import_box.ShowModal()
        import_box.Destroy()
        if retCode != wx.ID_OK: return
        # So the user wants to load in a csv file
        # Flag to other threads, that the network is being reloaded
        set_net_reload()
        # Create the load file dialog.
        # Pre-establish a file filter to show the .dat extension
        wildcard = "CSV file (*.csv)|*.csv|All files (*.*)|*.*"
        dlg = wx.FileDialog(self, message="Import file...", defaultDir=net_cfg["EXTERNAL_NETWORKS"],
                            defaultFile="", wildcard=wildcard, style=wx.OPEN)
        # Show the dialog and retrieve the user response.
        # If it is the OK response, save the network data.
        if dlg.ShowModal() == wx.ID_OK:
            filepath = dlg.GetPath()
            dlg.Destroy()
        else:
            unset_net_reload()
            return
        # Display a please wait message in the right menu area
        self.sp.display_please_wait()
        #
        # self.import_csv_renew_tree is called 5 seconds from now,
        # since if we called import_network() from here, this function would not return
        # until the job was done, and the app loop would not display the required message
        # The time delay also allows other threads to note that the network is reloading
        wx.FutureCall(5000, self.sp.import_csv_renew_tree, filepath)


    def help_button_pressed(self, event):
        """Help button pressed, display help message"""
        help_text="""
Choose an item from the left tree, and then
add or edit groups or hosts.

The Apply button applies any changes made.
There may be a delay until the next
polling interval before changes appear on
the web site.

Groups can contain hosts or further groups,
hosts are defined with their IP address.

-- Top buttons --

The New button deletes the current network,
and allows you to build a new one.

Having created a network, Save allows you
to save it as an external file, Load allows
you to load in a previously saved network
file.

Import allows you to add hosts from a csv
file. The file format is described when the
Import button is pressed. 

Use New and Load with caution as they will
overwrite and delete the current network.
"""
        wx.MessageBox(parent=self, message=help_text,
                      caption="Skipole Monitor Help",
                      style=wx.OK | wx.ICON_INFORMATION | wx.STAY_ON_TOP)



# ------------- The split window, holding the tree control on the left,
# ------------- and item menu options on the right

class SMSplitterWindow(wx.SplitterWindow):
    def __init__(self, parent):
        """Set up the split window, with a tree on the left, and menu on the right"""
        #
        wx.SplitterWindow.__init__(self, parent)
        #
        self.p_right=wx.Panel(self)
        self.p_left=wx.Panel(self)
        #
        self.SplitVertically(self.p_left, self.p_right, 200)
        self.SetMinimumPaneSize(200)
        self.SetMinSize((600,400))
        #
        # Create the right panel
        # add a 'please_wait' page, plus group and host menus into it,
        # starting with only the top group item visible
        #
        self.please_wait = PleaseWait(self.p_right)
        self.please_wait.Hide()
        self.host = host_menu.Host(self.p_right, self)
        self.host.Hide()
        self.group = group_menu.Group(self.p_right, self)
        #
        # Create the right panel sizer
        self.right_sizer=wx.BoxSizer(wx.VERTICAL)
        #
        # Only one of 'please_wait', group or host will be displayed at any
        # one time in the right panel, depending on which item is selected
        # in the tree
        #
        self.right_sizer.Add(self.please_wait)
        self.right_sizer.Add(self.group)
        self.right_sizer.Add(self.host)
        #
        self.p_right.SetSizer(self.right_sizer)
        #
        # Create the left tree control
        self.p_left.SetBackgroundColour("white")
        #
        # Create the tree as an instance of class NetworkTree
        self.tree=NetworkTree(self.p_left)
        #
        # set up the tree items
        self.setup_tree()
        #
        # Create the left panel sizer
        self.left_sizer=wx.BoxSizer(wx.VERTICAL)
        #
        self.left_sizer.Add(self.tree, 1, wx.EXPAND)
        #
        self.p_left.SetSizer(self.left_sizer)
        #
        # Create tree bindings
        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelected, self.tree)
        #
        #
        tree_root=self.tree.GetRootItem()
        self.tree.Expand(tree_root)
        self.tree.SelectItem(tree_root)
        #
        # Check status every twenty seconds, refreshing the tree images
        self.status_checker = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.SetImages, self.status_checker)
        self.status_checker.Start(20000, oneShot=False)
        #
        return
        

    def setup_tree(self):
        """Populate the tree with the network, from the net module"""
        # Create a dictionary linking tree items to network items
        self.tree_dev={}
        last_network_item=last_net()
        #
        # Create an image list
        il=wx.ImageList(20, 20, True, 8)
        # Red Group image
        red_group_png=os.path.join(net_cfg["IMAGES_FOLDER"],'redgroup.png')
        red_group_image= wx.Image(red_group_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_redgroup=il.Add(wx.BitmapFromImage(red_group_image))
        # Yellow Group image
        yellow_group_png=os.path.join(net_cfg["IMAGES_FOLDER"],'yellowgroup.png')
        yellow_group_image= wx.Image(yellow_group_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_yellowgroup=il.Add(wx.BitmapFromImage(yellow_group_image))
        # Green Group image
        green_group_png=os.path.join(net_cfg["IMAGES_FOLDER"],'greengroup.png')
        green_group_image= wx.Image(green_group_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_greengroup=il.Add(wx.BitmapFromImage(green_group_image))
        # White Group image
        white_group_png=os.path.join(net_cfg["IMAGES_FOLDER"],'whitegroup.png')
        white_group_image= wx.Image(white_group_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_whitegroup=il.Add(wx.BitmapFromImage(white_group_image))
        # Red Host image
        red_png=os.path.join(net_cfg["IMAGES_FOLDER"],'redhost.png')
        red_image= wx.Image(red_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_red=il.Add(wx.BitmapFromImage(red_image))
        # Yellow Host image
        yellow_png=os.path.join(net_cfg["IMAGES_FOLDER"],'yellowhost.png')
        yellow_image= wx.Image(yellow_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_yellow=il.Add(wx.BitmapFromImage(yellow_image))
        # Green Host image
        green_png=os.path.join(net_cfg["IMAGES_FOLDER"],'greenhost.png')
        green_image= wx.Image(green_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_green=il.Add(wx.BitmapFromImage(green_image))
        # White Host image
        white_png=os.path.join(net_cfg["IMAGES_FOLDER"],'whitehost.png')
        white_image= wx.Image(white_png, wx.BITMAP_TYPE_PNG).Scale(20, 20)
        self.il_white=il.Add(wx.BitmapFromImage(white_image))
        # Assign the image list to the tree
        self.tree.AssignImageList(il)
        # Create the root tree item
        self.tree_dev[1]=self.tree.AddRoot(get_net(1, NAME), data=None)
        self.tree.SetItemBold(self.tree_dev[1], bold=True)
        self.tree.SetItemPyData(self.tree_dev[1], 1)
        #
        for item in range(2,last_network_item+1):
            # The item with this particular id must exist (it could have been deleted)
            if not is_net_exists(item): continue
            self.tree_dev[item]=self.tree.AppendItem(self.tree_dev[get_net(item, PARENT)],
                                                     get_net(item, NAME), data=None)
            self.tree.SetItemPyData(self.tree_dev[item], item)
        # sort the tree
        for item in network(0):
            # Assign an initial white image to each item
            self.tree.SetItemImage(self.tree_dev[item], self.il_white, wx.TreeItemIcon_Normal)
            # if item is a group, set it to bold, and sort its children
            if is_net_group(item):
                self.tree.SetItemBold(self.tree_dev[item], bold=True)
                self.tree.SortChildren(self.tree_dev[item])
                # Assign an initial white image to each group item
                self.tree.SetItemImage(self.tree_dev[item], self.il_whitegroup, wx.TreeItemIcon_Normal)
            
    def SetImages(self, event):
        """Sets each item with an image depending on its current status, called every 20 seconds"""
        if net_reload(): return
        for item in network(0):
            # Set each item image
            if get_net(item, STATUS)==3:
                if is_net_group(item):
                    self.tree.SetItemImage(self.tree_dev[item], self.il_redgroup, wx.TreeItemIcon_Normal)
                else:
                    self.tree.SetItemImage(self.tree_dev[item], self.il_red, wx.TreeItemIcon_Normal)
            elif get_net(item, STATUS)==2:
                if is_net_group(item):
                    self.tree.SetItemImage(self.tree_dev[item], self.il_yellowgroup, wx.TreeItemIcon_Normal)
                else:
                    self.tree.SetItemImage(self.tree_dev[item], self.il_yellow, wx.TreeItemIcon_Normal)
            elif get_net(item, STATUS)==1:
                if is_net_group(item):
                    self.tree.SetItemImage(self.tree_dev[item], self.il_greengroup, wx.TreeItemIcon_Normal)
                else:
                    self.tree.SetItemImage(self.tree_dev[item], self.il_green, wx.TreeItemIcon_Normal)
            else:
                if is_net_group(item):
                    self.tree.SetItemImage(self.tree_dev[item], self.il_whitegroup, wx.TreeItemIcon_Normal)
                else:
                    self.tree.SetItemImage(self.tree_dev[item], self.il_white, wx.TreeItemIcon_Normal)
        # Update graph if a host menu is being displayed
        self.host.update_graph()
                
                
    def OnSelected(self, event):
        """Called when a tree item is selected, creates appropriate right panel"""
        #return
        if net_reload(): return
        # select a group or a host
        item=self.tree.GetItemPyData(event.GetItem())
        # create the right panel
        # Find the new right panel to display, either group or host
        #
        if is_net_group(item):
            self.group.set_item(item)
            self.please_wait.Hide()
            self.host.Hide()
            self.group.Show()
        else:
            self.host.set_item(item)
            self.please_wait.Hide()
            self.group.Hide()
            self.host.Show()
            #
            # Update graph
            self.host.update_graph()
        self.right_sizer.Layout()
        return


    # ---------- Actions initiated from the Top buttons


    def display_please_wait(self):
        """Clears panels, and shows a please wait while the monitor reloads"""
        self.group.Hide()
        self.host.Hide()
        self.please_wait.Show()
        self.right_sizer.Layout()

    def delete_net_renew_tree(self):
        """delete network and renew the tree, with only the top item"""
        # The delete_network function is in the net module
        # it deletes the current network, and replaces it with
        # a network consisting of the top item only
        delete_network()
        #
        # now delete the existing tree
        self.tree.DeleteAllItems()
        # Then setup the new tree - which will only consist of the top item
        self.setup_tree()
        tree_root=self.tree.GetRootItem()
        self.tree.Expand(tree_root)
        self.tree.SelectItem(tree_root)
        #
        self.group.set_item(1)
        self.host.Hide()
        self.please_wait.Hide()
        self.group.Show()
        self.right_sizer.Layout()

    def load_net_renew_tree(self, filepath):
        """Renew network, delete existing tree and setup a new tree panel"""
        # The load_saved_network function is in the net module
        # it deletes the current network, and imports a previously saved network
        load_saved_network(filepath)
        #
        # delete the existing tree
        self.tree.DeleteAllItems()
        # Then setup the new tree
        self.setup_tree()
        tree_root=self.tree.GetRootItem()
        self.tree.Expand(tree_root)
        self.tree.SelectItem(tree_root)
        #
        self.group.set_item(1)
        self.host.Hide()
        self.please_wait.Hide()
        self.group.Show()
        self.right_sizer.Layout()


    def import_csv_renew_tree(self, filepath):
        """Load in a csv file, delete existing tree and setup a new tree"""
        # The import_csv_network function is in the net module
        # it reads the csv file, parses it and imports the hosts within
        # it returns a list of two objects
        # On success - True, Number of lines read
        # On failure - False, line number of failure
        #
        result, line = import_csv_network(filepath)
        if not result:
            wx.MessageBox(parent=self,
                          message="ERROR: The CSV file has an error at line " + str(line),
                          caption="ERROR",
                          style=wx.OK | wx.ICON_ERROR | wx.STAY_ON_TOP)
            return
        #
        # delete the existing tree
        self.tree.DeleteAllItems()
        # Then setup the new tree
        self.setup_tree()
        tree_root=self.tree.GetRootItem()
        self.tree.Expand(tree_root)
        self.tree.SelectItem(tree_root)
        #
        self.group.set_item(1)
        self.host.Hide()
        self.please_wait.Hide()
        self.group.Show()
        self.right_sizer.Layout()


# -------- A custom subclass of the TreeCtrl, providing fancy sorting

class NetworkTree(wx.TreeCtrl):
    """A custom subclass of the TreeCtrl, providing fancy sorting"""
    def OnCompareItems(self, item1, item2):
        """Causes hosts to be sorted before groups"""
        data1=self.GetItemPyData(item1)
        data2=self.GetItemPyData(item2)
        if is_net_group(data1) and is_net_group(data2):
            # Both items groups, so sort alphabetically on group name
            if get_net(data1, NAME) < get_net(data2, NAME): return -1
            if get_net(data1, NAME) == get_net(data2, NAME): return 0
            return 1
        if is_net_group(data1): return 1
        if is_net_group(data2): return -1
        # Both items hosts, so sort alphabetically on host name
        if get_net(data1, NAME) < get_net(data2, NAME): return -1
        if get_net(data1, NAME) == get_net(data2, NAME): return 0
        return 1



# ---------------- PleaseWait Class - displays a please wait message

class PleaseWait(wx.Panel):
    """Displays Please Wait message"""
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        self.menu_sizer=wx.BoxSizer(wx.HORIZONTAL)
        # Add a text message
        monitor_reload_text=wx.StaticText(self, -1, "Please wait while the monitor reloads")
        self.menu_sizer.Add(monitor_reload_text, 0, wx.ALL, 50)
        # Set the size to the panel
        self.SetSizer(self.menu_sizer)


# ------------------ Separate Frame showing import csv file information

class ImportCSV(wx.Dialog):
    """Displays a Frame with the import CSV information""" 
    def __init__(self, parent):
        wx.Dialog.__init__(self, None, -1, "Import CSV File",)
        #
        # Set the frame icon
        #
        self.SetIcon(gui_globals.get_icon())
        #
        self.import_panel = wx.Panel(self, -1)
        #
        # Create the sizer for the frame
        self.import_sizer=wx.BoxSizer(wx.VERTICAL)
        #
        # Create the text to display
        #
        text = """
If you have a list of hosts you wish to import, and you can put them into
a csv file, this option allows you to add them to the current network.

The csv file should have the following format - one line for each host,
each line consisting of the comma separated fields:

 1st field: The short name of the group this host will be added to.
 2nd field: The IP address of the host.
 3rd field: The short name of the host, 12 characters maximum.
 4th field: The host description, 80 characters maximum.

The short name and description should not contain commas.
If the group does not exist, a new group with that short name will be
created. (12 characters maximum)

Example:

Top,192.168.1.1,First host,This host is in the top group
Top,192.168.1.8,Second host,This host is also in the top group
Subgroup,192.168.1.9,Third host,This host is in this subgroup
Subgroup,192.168.1.10,Fourth host,Another host in the subgroup
Top,192.168.1.11,Fifth host,This host is in the top group

Press OK if you want to import such a file.
"""
        #
        self.top_text=wx.StaticText(self.import_panel, -1, text)
        self.import_sizer.Add(self.top_text, 0, wx.ALL, 20)
        #
        #
        # Create a horizontal button_sizer to hold the buttons
        self.button_sizer=wx.BoxSizer(wx.HORIZONTAL)
        #
        button_size = (100,40)
        #
        self.cancelButton = wx.Button(self.import_panel, wx.ID_CANCEL, size=button_size)  
        self.button_sizer.Add(self.cancelButton, 0, wx.BOTTOM, 20)
        #
        # add a bit of space
        self.button_sizer.Add(button_size, 1)
        #
        self.okButton = wx.Button(self.import_panel, wx.ID_OK, size=button_size)
        self.button_sizer.Add(self.okButton, 0, wx.BOTTOM, 20)
        #
        # Add button_sizer to the import sizer
        self.import_sizer.Add(self.button_sizer, 0, wx.ALIGN_CENTER)
        #
        # Set the size to the panel, and frame
        self.import_panel.SetSizer(self.import_sizer)
        self.import_sizer.Fit(self)
        self.import_sizer.SetSizeHints(self)


